package org.jinghouyu.http.proxy.stream;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import org.apache.log4j.Logger;
import org.jinghouyu.http.proxy.utils.ByteAppender;
import org.jinghouyu.http.proxy.utils.EncodingUtil;

/**
 * was changed from ChunkedInputStream
 * why does this class exists? 
 * when we read datas from socket input stream. we must know when the stream was reached to the end.
 * chunkedInputStream knows that only while the transfer-encoding is "chunked"!
 * chunkedInputStream will drop the meta data, but in lots of situation, we need know both of meta data and the stream's end.
 * it is just the reason ChunkedNoRenderInputStream exists.
 * 
 * @author liujingyu
 *
 */
public class ChunkedNoRenderInputStream extends InputStream {

	/** The inputstream that we're wrapping */
	private InputStream in;

	/** The chunk size */
	private int chunkSize;

	/** The current position within the current chunk */
	private int pos;

	/** True if we'are at the beginning of stream */
	private boolean bof = true;

	/** True if we've reached the end of stream */
	private boolean eof = false;

	/** True if this stream is closed */
	private boolean closed = false;

	/** Log object for this class. */
	private static final Logger LOG = Logger.getLogger(ChunkedInputStream.class);

	/**
	 * ChunkedInputStream constructor that associates the chunked input stream
	 * with a {@link HttpMethod HTTP method}. Usually it should be the same
	 * {@link HttpMethod HTTP method} the chunked input stream originates from.
	 * If chunked input stream contains any footers (trailing headers), they
	 * will be added to the associated {@link HttpMethod HTTP method}.
	 * 
	 * @param in
	 *            the raw input stream
	 * @param method
	 *            the HTTP method to associate this input stream with. Can be
	 *            <tt>null</tt>.
	 * 
	 * @throws IOException
	 *             If an IO error occurs
	 */
	public ChunkedNoRenderInputStream(InputStream in) throws IOException {

		if (in == null) {
			throw new IllegalArgumentException(
					"InputStream parameter may not be null");
		}
		this.in = in;
		this.pos = 0;
	}

	/**
	 * <p>
	 * Returns all the data in a chunked stream in coalesced form. A chunk is
	 * followed by a CRLF. The method returns -1 as soon as a chunksize of 0 is
	 * detected.
	 * </p>
	 * 
	 * <p>
	 * Trailer headers are read automcatically at the end of the stream and can
	 * be obtained with the getResponseFooters() method.
	 * </p>
	 * 
	 * @return -1 of the end of the stream has been reached or the next data
	 *         byte
	 * @throws IOException
	 *             If an IO problem occurs
	 * 
	 * @see HttpMethod#getResponseFooters()
	 */
	public int read() throws IOException {

		if (closed) {
			throw new IOException("Attempted read from closed stream.");
		}
		if(tmpPos < tmpDatas.length) {
			return tmpDatas[tmpPos++];
		}
		if (eof) {
			return -1;
		}
		if (pos >= chunkSize) {
			nextChunk();
			if(tmpPos < tmpDatas.length) {
				return tmpDatas[tmpPos++];
			}
			if (eof) {
				return -1;
			}
		}
		pos++;
		return in.read();
	}

	/**
	 * Read some bytes from the stream.
	 * 
	 * @param b
	 *            The byte array that will hold the contents from the stream.
	 * @param off
	 *            The offset into the byte array at which bytes will start to be
	 *            placed.
	 * @param len
	 *            the maximum number of bytes that can be returned.
	 * @return The number of bytes returned or -1 if the end of stream has been
	 *         reached.
	 * @see java.io.InputStream#read(byte[], int, int)
	 * @throws IOException
	 *             if an IO problem occurs.
	 */
	public int read(byte[] b, int off, int len) throws IOException {
		if (closed) {
			throw new IOException("Attempted read from closed stream.");
		}
		return super.read(b, off, len);
	}

	/**
	 * Read some bytes from the stream.
	 * 
	 * @param b
	 *            The byte array that will hold the contents from the stream.
	 * @return The number of bytes returned or -1 if the end of stream has been
	 *         reached.
	 * @see java.io.InputStream#read(byte[])
	 * @throws IOException
	 *             if an IO problem occurs.
	 */
	public int read(byte[] b) throws IOException {
		return read(b, 0, b.length);
	}

	/**
	 * Read the CRLF terminator.
	 * 
	 * @throws IOException
	 *             If an IO error occurs.
	 */
	private void readCRLF() throws IOException {
		int cr = in.read();
		int lf = in.read();
		if ((cr != '\r') || (lf != '\n')) {
			throw new IOException("CRLF expected at end of chunk: " + cr + "/"
					+ lf);
		}
	}

	private byte[] tmpDatas = new byte[0];
	private int tmpPos = 0;
	
	/**
	 * Read the next chunk.
	 * 
	 * @throws IOException
	 *             If an IO error occurs.
	 */
	private void nextChunk() throws IOException {
		ByteAppender appender = new ByteAppender();
		if (!bof) {
			readCRLF();
			appender.append((byte) '\r');
			appender.append((byte) '\n');
		}
		Map<String, Object> map = getChunkSizeFromInputStream(in);
		byte[] datas = (byte[]) map.get("datas");
		appender.append(datas);
		chunkSize = (int) map.get("size");
		if(chunkSize == 0) {
			appender.append((byte) '\r');
			appender.append((byte) '\n');
		}
		bof = false;
		pos = 0;
		tmpPos = 0;
		tmpDatas = appender.getDatas();
		if (chunkSize == 0) {
			eof = true;
			parseTrailerHeaders();
		}
	}

	/**
	 * Expects the stream to start with a chunksize in hex with optional
	 * comments after a semicolon. The line must end with a CRLF: "a3; some
	 * comment\r\n" Positions the stream at the start of the next line.
	 * 
	 * @param in
	 *            The new input stream.
	 * @param required
	 *            <tt>true<tt/> if a valid chunk must be present,
	 *                 <tt>false<tt/> otherwise.
	 * 
	 * @return the chunk size as integer
	 * 
	 * @throws IOException
	 *             when the chunk size could not be parsed
	 */
	private static Map<String, Object> getChunkSizeFromInputStream(
			final InputStream in) throws IOException {
		Map<String, Object> result = new HashMap<String, Object>();
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		ByteAppender appender = new ByteAppender();
		// States: 0=normal, 1=\r was scanned, 2=inside quoted string, -1=end
		int state = 0;
		while (state != -1) {
			int b = in.read();
			if (b == -1) {
				throw new IOException("chunked stream ended unexpectedly");
			}
			appender.append((byte) b);
			switch (state) {
			case 0:
				switch (b) {
				case '\r':
					state = 1;
					break;
				case '\"':
					state = 2;
					/* fall through */
				default:
					baos.write(b);
				}
				break;

			case 1:
				if (b == '\n') {
					state = -1;
				} else {
					// this was not CRLF
					throw new IOException("Protocol violation: Unexpected"
							+ " single newline character in chunk size");
				}
				break;

			case 2:
				switch (b) {
				case '\\':
					b = in.read();
					baos.write(b);
					break;
				case '\"':
					state = 0;
					/* fall through */
				default:
					baos.write(b);
				}
				break;
			default:
				throw new RuntimeException("assertion failed");
			}
		}

		// parse data
		String dataString = EncodingUtil.getAsciiString(baos.toByteArray());
		int separator = dataString.indexOf(';');
		dataString = (separator > 0) ? dataString.substring(0, separator)
				.trim() : dataString.trim();

		try {
			int size = Integer.parseInt(dataString.trim(), 16);
			result.put("size", size);
			result.put("datas", appender.getDatas());
		} catch (NumberFormatException e) {
			throw new IOException("Bad chunk size: " + dataString);
		}
		return result;
	}

	/**
	 * Reads and stores the Trailer headers.
	 * 
	 * @throws IOException
	 *             If an IO problem occurs
	 */
	private void parseTrailerHeaders() throws IOException {
		try {
			String charset = "US-ASCII";
			skipHeaders(charset);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	private void skipHeaders(String charset) throws IOException {
		LOG.trace("enter HeaderParser.parseHeaders(InputStream, String)");

		for (;;) {
			String line = readLine(charset);
			if ((line == null) || (line.trim().length() < 1)) {
				break;
			}

			if ((line.charAt(0) == ' ') || (line.charAt(0) == '\t')) {
			} else {
				int colon = line.indexOf(":");
				if (colon < 0) {
					throw new RuntimeException("Unable to parse header: "
							+ line);
				}
			}

		}
	}

	private String readLine(String charset) throws IOException {
		LOG.trace("enter HttpParser.readLine(InputStream, String)");
		byte[] rawdata = readRawLine(in);
		if (rawdata == null) {
			return null;
		}
		// strip CR and LF from the end
		int len = rawdata.length;
		int offset = 0;
		if (len > 0) {
			if (rawdata[len - 1] == '\n') {
				offset++;
				if (len > 1) {
					if (rawdata[len - 2] == '\r') {
						offset++;
					}
				}
			}
		}
		final String result = EncodingUtil.getString(rawdata, 0, len - offset,
				charset);
		return result;
	}

	private static byte[] readRawLine(InputStream inputStream)
			throws IOException {
		LOG.trace("enter HttpParser.readRawLine()");

		ByteArrayOutputStream buf = new ByteArrayOutputStream();
		int ch;
		while ((ch = inputStream.read()) >= 0) {
			buf.write(ch);
			if (ch == '\n') { // be tolerant (RFC-2616 Section 19.3)
				break;
			}
		}
		if (buf.size() == 0) {
			return null;
		}
		return buf.toByteArray();
	}

	/**
	 * Upon close, this reads the remainder of the chunked message, leaving the
	 * underlying socket at a position to start reading the next response
	 * without scanning.
	 * 
	 * @throws IOException
	 *             If an IO problem occurs.
	 */
	public void close() throws IOException {
		if (!closed) {
			try {
				if (!eof) {
					exhaustInputStream(this);
				}
			} finally {
				eof = true;
				closed = true;
			}
		}
	}

	/**
	 * Exhaust an input stream, reading until EOF has been encountered.
	 * 
	 * <p>
	 * Note that this function is intended as a non-public utility. This is a
	 * little weird, but it seemed silly to make a utility class for this one
	 * function, so instead it is just static and shared that way.
	 * </p>
	 * 
	 * @param inStream
	 *            The {@link InputStream} to exhaust.
	 * @throws IOException
	 *             If an IO problem occurs
	 */
	static void exhaustInputStream(InputStream inStream) throws IOException {
		// read and discard the remainder of the message
		byte buffer[] = new byte[1024];
		while (inStream.read(buffer) >= 0) {
			;
		}
	}
}
